﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Constants = EnvDTE.Constants;

namespace ThreadSafe.JSense.VSIX
{
   class JSenseCommand
   {
      private readonly DTE2 _applicationObject;
      private List<ProjectItem> _rootItems;
      private List<string> _exclusions;

      private EditPoint _metaBaseEditPoint;
      private const string JSenseMetaBase = "@JSense.js";
      private const string JSenseErrorLog = "@JSense.log";

      private const string JSenseExcludeTag = "/// <JSense exclude=\"{0}\" />\r\n";
      private const string JSenseReferenceTag = "/// <reference path=\"/@JSense.js\" />";
      private const string MetaBaseReferenceTag = "/// <reference path=\"{0}\" />\r\n";

      private const string ExcludeTagPattern = "(?<exclTag>\\/\\/\\/\\s?<JSense\\s+exclude=\"(?<exclAttr>.*?)\"\\s?\\/>)";

      public JSenseCommand(object application)
      {
         _applicationObject = (DTE2)application;
      }

      public bool JSense()
      {
         _exclusions = new List<string>();

         try
         {
            var projects = (Array)_applicationObject.ActiveSolutionProjects;
            if (projects.Length == 0) return true;

            var project = (Project)(projects.GetValue(0));
            _rootItems = project.ProjectItems.Cast<ProjectItem>().ToList();

            if (_rootItems.Exists(i => i.Name == JSenseMetaBase))
               InitializeMetaBase(_rootItems.First(i => i.Name == JSenseMetaBase));
            else
               CreateMetaBase();

            _rootItems.ForEach(i => ProcessProjectItem(i, ""));
            _metaBaseEditPoint.Parent.Parent.Save();

            return true;
         }
         catch (Exception e)
         {
            HandleError(e);
            Trace.WriteLine(e.Message);
            return false;
         }
      }

      private void HandleError(Exception e)
      {
         var jSenseErrorLog = _rootItems.FirstOrDefault(i => i.Name == JSenseErrorLog) ??
            _applicationObject.ItemOperations.AddNewItem("General\\Text File", JSenseErrorLog);

         var window = jSenseErrorLog.Open(Constants.vsViewKindCode);
         var textDocument = (TextDocument)window.Document.Object("TextDocument");
         var editPoint = textDocument.EndPoint.CreateEditPoint();

         editPoint.Insert("\tError Message:\r\n");
         editPoint.Insert("\t" + e.Message + "\r\n\r\n");
         editPoint.Insert("\tStack Trace:\r\n");
         editPoint.Insert("\t" + e.StackTrace + "\r\n\r\n");

         jSenseErrorLog.Save();
      }

      private void CreateMetaBase()
      {
         var projectItem = _applicationObject.ItemOperations.AddNewItem("General\\Text File", JSenseMetaBase);
         var window = projectItem.Open(Constants.vsViewKindCode);
         var textDocument = (TextDocument)window.Document.Object("TextDocument");
         _metaBaseEditPoint = textDocument.StartPoint.CreateEditPoint();

         AddDefaultExclusions();

         projectItem.Save();
      }

      private void AddDefaultExclusions()
      {
         AddMetaBaseHeader();

         _metaBaseEditPoint.Insert(string.Format(JSenseExcludeTag, ".*\\.min\\.js"));
         _metaBaseEditPoint.Insert(string.Format(JSenseExcludeTag, ".*\\.asmx"));
         _metaBaseEditPoint.Insert(string.Format(JSenseExcludeTag, ".*\\.aspx"));
         _metaBaseEditPoint.Insert("\r\n\r\n");

         _exclusions.Add(".*\\.min\\.js");
         _exclusions.Add(".*\\.asmx");
         _exclusions.Add(".*\\.aspx");
      }

      private void AddMetaBaseHeader()
      {
         _metaBaseEditPoint.Insert("\r\n\r\n");
         _metaBaseEditPoint.Insert("/// JSense excludes minified JavaScript, Web Services and Web Forms by default.\r\n");
         _metaBaseEditPoint.Insert("/// Remove the exclusion tag below to include these types. You may also include your own exclusion tags \r\n");
         _metaBaseEditPoint.Insert("/// using a regular expression pattern. Please note that JSense tags require tripple leading slashes ///.\r\n\r\n");

         _metaBaseEditPoint.Insert("/// example: <JSense exclude=\".*[^\\.debug]\\.js\" /> will exclude all .js files NOT marked .debug.js\r\n\r\n");
      }

      private void InitializeMetaBase(ProjectItem metaBase)
      {
         var window = metaBase.Open(Constants.vsViewKindCode);
         var textDocument = (TextDocument)window.Document.Object("TextDocument");
         _metaBaseEditPoint = textDocument.StartPoint.CreateEditPoint();

         LoadExclusions();
         ClearMetaBase();
         AddMetaBaseHeader();
         AddExclusions();

         metaBase.Save();
      }

      private void LoadExclusions()
      {
         var text = _metaBaseEditPoint.GetText(_metaBaseEditPoint.Parent.EndPoint);

         var regex = new Regex(ExcludeTagPattern,
            RegexOptions.IgnoreCase | RegexOptions.Multiline);

         var match = regex.Match(text);
         while (match.Success)
         {
            _exclusions.Add(match.Groups["exclAttr"].Value);
            match = match.NextMatch();
         }
      }

      private void ClearMetaBase()
      {
         _metaBaseEditPoint.Delete(_metaBaseEditPoint.Parent.EndPoint);
      }

      private void AddExclusions()
      {
         foreach (var exclusion in _exclusions)
            _metaBaseEditPoint.Insert(string.Format(JSenseExcludeTag, exclusion));

         _metaBaseEditPoint.Insert("\r\n\r\n");
      }

      private void ProcessProjectItem(ProjectItem projecttItem, string path)
      {
         foreach (ProjectItem childItem in projecttItem.ProjectItems)
            ProcessProjectItem(childItem, path + "/" + projecttItem.Name);

         var fullPath = string.IsNullOrEmpty(path)
            ? projecttItem.Name
            : path + "/" + projecttItem.Name;

         if (!projecttItem.Name.EndsWith(".js") &&
             !projecttItem.Name.EndsWith(".asmx") &&
             !projecttItem.Name.EndsWith(".aspx")) return;

         if (projecttItem.Name == "@JSense.js") return;

         if (IsExcluded(fullPath)) return;

         var window = projecttItem.Open(Constants.vsViewKindCode);
         var textDocument = (TextDocument)window.Document.Object("TextDocument");
         var editPoint = textDocument.StartPoint.CreateEditPoint();

         if (!editPoint.FindPattern(JSenseReferenceTag))
         {
            editPoint.StartOfDocument();
            editPoint.Insert(JSenseReferenceTag);
            editPoint.Insert("\r\n");

            projecttItem.Save();
         }

         TagMetaBase(fullPath);
      }

      private void TagMetaBase(string path)
      {
         var startEditPoint = _metaBaseEditPoint.Parent.StartPoint.CreateEditPoint();
         startEditPoint.Insert(string.Format(MetaBaseReferenceTag, path));
      }

      private bool IsExcluded(string path)
      {
         return _exclusions.Any(exclusion => new Regex(exclusion).IsMatch(path));
      }

   }
}
